#region license
/*
 * Copyright (C) 2007 CchenSoft.com
 * 
 * This library is free software; you can redistribute it and/or modify it 
 * under the terms of the GNU Lesser General Public License 2.1 or later, as
 * published by the Free Software Foundation. See the included License.txt
 * or http://www.gnu.org/copyleft/lesser.html for details.
 * 
 * Author: Billy Zhang
 */
#endregion

using System.Collections;
using System;
using System.Collections.Generic;

using log4net;
using NHibernate;
using NHibernate.Cfg;
using NHibernate.Criterion;

using CchenSoft.Workflow.Query;

namespace CchenSoft.Workflow.Spi.Hibernate
{
    /**
     * A workflow store backed by Hibernate for persistence.  To use this with the standard
     * persistence factory, pass to the DefaultConfiguration.persistenceArgs the SessionFactory to
     * use:
     * <code>DefaultConfiguration.persistenceArgs.put("sessionFactory", DatabaseHelper.getSessionFactory());</code>
     * See the HibernateFunctionalWorkflowTestCase for more help.
     *
     */
    public class HibernateWorkflowStore : IWorkflowStore
    {
        //~ Static fields/initializers /////////////////////////////////////////////

        private static ILog log = LogManager.GetLogger(typeof(HibernateWorkflowStore));

        //~ Instance fields ////////////////////////////////////////////////////////

        ISession session;
        ISessionFactory sessionFactory;

        //~ Constructors ///////////////////////////////////////////////////////////

        public HibernateWorkflowStore()
        {
        }

        public HibernateWorkflowStore(ISessionFactory sessionFactory)
        {
            this.sessionFactory = sessionFactory;

            try
            {
                this.session = sessionFactory.OpenSession();
            }
            catch (HibernateException he)
            {
                log.Error("constructor", he);
                throw new StoreException("constructor", he);
            }
        }

        private ITransaction trans = null;

        public int BeginTransaction()
        {
            if (trans == null)
            {
                trans = session.BeginTransaction();
                return 1;
            }
            return 0;
        }

        public void RollbackTransaction()
        {
            trans.Rollback();
            trans = null;
        }

        public void CommitTransaction()
        {
            trans.Commit();
            trans = null;
        }

        //~ Methods ////////////////////////////////////////////////////////////////

        public void SetEntryState(long entryId, int state)
        {
            try
            {
                HibernateWorkflowEntry entry = session.Load<HibernateWorkflowEntry>(entryId);
                entry.State = state;
                session.Save(entry);
            }
            catch (HibernateException e)
            {
                log.Error("An exception occured", e);

                return;
            }
        }
      
        public IStep CreateCurrentStep(long entryId, int stepId, string owner, DateTime startDate, DateTime? dueDate, string status, long[] previousIds)
        {
            HibernateCurrentStep step = new HibernateCurrentStep();
            HibernateWorkflowEntry entry;

            //ITransaction tx;

            try
            {
                //tx = session.BeginTransaction();
                entry = session.Load<HibernateWorkflowEntry>(entryId);
            }
            catch (HibernateException he)
            {
                log.Error("Looking for workflow entry " + entryId, he);
                throw new StoreException("Looking for workflow entry " + entryId, he);
            }

            step.Entry = entry;
            step.StepId = stepId;
            step.Owner = owner;
            step.StartDate = startDate;
            step.DueDate = dueDate;
            step.Status = status;

            string[] stepIds = new string[previousIds.Length];

            for (int i = 0; i < previousIds.Length; i++)
            {
                stepIds[i] = previousIds[i].ToString();
            }

            if (stepIds.Length > 0)
            {
                string str = string.Join(", ", stepIds);

                try
                {
                    string hql = string.Format("FROM {0} WHERE Id IN ({1})",typeof(HibernateHistoryStep).Name, str);
                    step.PreviousSteps = session.CreateQuery(hql).List();
                }
                catch (HibernateException he)
                {
                    log.Error("Looking for step in " + stepIds, he);
                    throw new StoreException("Looking for step in " + stepIds, he);
                }
            }
            else
            {
                step.PreviousSteps = new ArrayList(0);
            }

            if (entry.CurrentSteps == null)
            {
                List<IStep> cSteps = new List<IStep>(1);
                cSteps.Add(step);
                entry.CurrentSteps = cSteps;
            }
            else
            {
                entry.CurrentSteps.Add(step);
            }

            try
            {
                session.Save(entry);
                //tx.Commit();

                //session.save(step);
                return step;
            }
            catch (HibernateException he)
            {
                log.Error("Saving new workflow entry", he);
                throw new StoreException("Saving new workflow entry", he);
            }
        }

        public IWorkflowEntry CreateEntry(string workflowName)
        {
            HibernateWorkflowEntry entry = new HibernateWorkflowEntry();
            entry.State = WorkflowEntryState.CREATED;
            entry.WorkflowName = workflowName;

            //ITransaction tx;

            try
            {
                //tx = session.BeginTransaction();
                session.Save(entry);
                //tx.Commit();
            }
            catch (HibernateException he)
            {
                log.Error("Saving new workflow entry", he);
                throw new StoreException("Saving new workflow entry", he);
            }

            return entry;
        }

        public IList<IStep> FindCurrentSteps(long entryId)
        {
            HibernateWorkflowEntry entry;

            try
            {
                entry = session.Load<HibernateWorkflowEntry>(entryId);
            }
            catch (HibernateException he)
            {
                log.Error("Looking for entryId " + entryId, he);
                throw new StoreException("Looking for entryId " + entryId, he);
            }

            try
            {
                string hql = string.Format("FROM {0} step WHERE step.Entry.Id = ? order by step.Id desc", typeof(HibernateCurrentStep).Name);
                IQuery query = session.CreateQuery(hql).SetInt64(0, entry.Id);
                return query.List<IStep>();                
            }
            catch (HibernateException he)
            {
                log.Error("Looking for step id " + entry, he);
                throw new StoreException("Looking for step id " + entry, he);
            }
        }

        public IWorkflowEntry FindEntry(long entryId)
        {
            try
            {
                return session.Load<HibernateWorkflowEntry>(entryId);
            }
            catch (HibernateException he)
            {
                log.Error("Looking for entry " + entryId, he);
                throw new StoreException("Loooking for entry " + entryId, he);
            }
        }

        public IList<IStep> FindHistorySteps(long entryId)
        {
            HibernateWorkflowEntry entry;

            try
            {
                entry = session.Load<HibernateWorkflowEntry>(entryId);
            }
            catch (HibernateException he)
            {
                log.Error("Finding entry " + entryId, he);
                throw new StoreException("Finding entry " + entryId, he);
            }

            try
            {
                string hql = string.Format("FROM {0} step WHERE step.Entry.Id = ? order by step.Id desc", typeof(HibernateHistoryStep).Name);
                IQuery query = session.CreateQuery(hql).SetInt64(0, entry.Id);
                return query.List<IStep>();
            }
            catch (HibernateException he)
            {
                log.Error("Looking for step with entry " + entry, he);
                throw new StoreException("Looking for step with entry " + entry, he);
            }
        }

        public void Init(IDictionary props)
        {
            try
            {
                Configuration cfg = new Configuration().Configure();
                cfg.AddClass(typeof(HibernateWorkflowEntry));
                cfg.AddClass(typeof(HibernateCurrentStep));
                cfg.AddClass(typeof(HibernateHistoryStep));

                sessionFactory = cfg.BuildSessionFactory();
                session = sessionFactory.OpenSession();
            }
            catch (HibernateException he)
            {
                log.Error("Setting sessionFactory", he);
                throw new StoreException("Setting sessionFactory", he);
            }
        }

        public IStep MarkFinished(IStep step, int actionId, DateTime? finishDate, string status, string caller)
        {
            HibernateCurrentStep currentStep = (HibernateCurrentStep)step;

            currentStep.ActionId = actionId;
            currentStep.FinishDate = finishDate;
            currentStep.Status = status;
            currentStep.Caller = caller;

            try
            {
                //ITransaction tx = session.BeginTransaction();
                session.Save(currentStep);
                //tx.Commit();

                return currentStep;
            }
            catch (HibernateException he)
            {
                log.Error("Saving current step with action " + actionId, he);
                throw new StoreException("Saving current step with action " + actionId, he);
            }
        }

        public void MoveToHistory(IStep step)
        {
            HibernateWorkflowEntry entry;

            //ITransaction tx;

            try
            {
                //tx = session.BeginTransaction();
                entry = session.Load<HibernateWorkflowEntry>(step.EntryId);
            }
            catch (HibernateException he)
            {
                log.Error("Looking for workflow entry " + step.EntryId, he);
                throw new StoreException("Looking for workflow entry " + step.EntryId, he);
            }

            HibernateHistoryStep hStep = new HibernateHistoryStep((HibernateStep)step);

            entry.CurrentSteps.Remove(step);

            if (entry.HistorySteps == null)
            {
                List<IStep> hSteps = new List<IStep>(1);
                hSteps.Add(hStep);
                entry.HistorySteps = hSteps;
            }
            else
            {
                entry.HistorySteps.Add(hStep);
            }

            try
            {
                session.Save(hStep);
                session.Save(entry);
                //tx.Commit();

                //session.delete(step);
                //session.save(hStep, new Long(hStep.Id));
            }
            catch (HibernateException he)
            {
                log.Error("Saving workflow entry " + entry.Id, he);
                throw new StoreException("Saving workflow entry " + entry.Id, he);
            }
        }

        public IList<long> Query(WorkflowExpressionQuery query)
        {
            Query.Expression expression = query.Expression;

            ICriterion expr;

            Type entityClass = GetQueryClass(expression, null);

            if (expression.IsNested())
            {
                expr = BuildNested((Query.NestedExpression)expression);
            }
            else
            {
                expr = QueryComparison((Query.FieldExpression)expression);
            }

            //get results and send them back
            ICriteria criteria = session.CreateCriteria(entityClass);
            criteria.Add(expr);

            try
            {
                List<long> results = new List<long>();

                IEnumerator iter = criteria.List().GetEnumerator();

                while (iter.MoveNext())
                {
                    object next = iter.Current;
                    long item;

                    if (next is HibernateStep)
                    {
                        HibernateStep step = (HibernateStep)next;
                        item = step.EntryId;
                    }
                    else
                    {
                        IWorkflowEntry entry = (IWorkflowEntry)next;
                        item = entry.Id;
                    }

                    results.Add(item);
                }

                return results;
            }
            catch (HibernateException e)
            {
                throw new StoreException("Error executing query " + expression, e);
            }
        }

        public IList<long> Query(WorkflowQuery query)
        {
            Type entityClass;

            int qtype = query.Type;

            if (qtype == 0)
            { // then not set, so look in sub queries

                if (query.Left != null)
                {
                    qtype = query.Left.Type;
                }
            }

            if (qtype == WorkflowQuery.CURRENT)
            {
                entityClass = typeof(HibernateCurrentStep);
            }
            else
            {
                entityClass = typeof(HibernateHistoryStep);
            }

            ICriteria criteria = session.CreateCriteria(entityClass);
            ICriterion expression = BuildExpression(query);
            criteria.Add(expression);

            //get results and send them back
            try
            {
                List<long> results = new List<long>();
                IList<HibernateStep> steps = criteria.List<HibernateStep>();

                foreach (HibernateStep step in steps)
                {
                    results.Add(step.EntryId);
                }

                return results;
            }
            catch (HibernateException e)
            {
                throw new StoreException("Error executing query " + expression, e);
            }
        }

        /**
         * Returns an expression generated from this query
         */
        private ICriterion GetExpression(WorkflowQuery query)
        {
            switch (query.Operator)
            {
                case WorkflowQuery.EQUALS:
                    return NHibernate.Criterion.Expression.Eq(GetFieldName(query.Field), query.Value);

                case WorkflowQuery.NOT_EQUALS:
                    return NHibernate.Criterion.Expression.Not(NHibernate.Criterion.Expression.Like(GetFieldName(query.Field), query.Value));

                case WorkflowQuery.GT:
                    return NHibernate.Criterion.Expression.Gt(GetFieldName(query.Field), query.Value);

                case WorkflowQuery.LT:
                    return NHibernate.Criterion.Expression.Lt(GetFieldName(query.Field), query.Value);

                default:
                    return NHibernate.Criterion.Expression.Eq(GetFieldName(query.Field), query.Value);
            }
        }

        /**
         * returns the correct name of the field given or "1" if none is found
         * which matches the input.
         * @param field
         * @return
         */
        private String GetFieldName(int field)
        {
            switch (field)
            {
                case FieldExpression.ACTION: // actionId
                    return "ActionId";

                case FieldExpression.CALLER:
                    return "Caller";

                case FieldExpression.FINISH_DATE:
                    return "FinishDate";

                case FieldExpression.OWNER:
                    return "Owner";

                case FieldExpression.START_DATE:
                    return "StartDate";

                case FieldExpression.STEP: // stepId
                    return "StepId";

                case FieldExpression.STATUS:
                    return "Status";

                case FieldExpression.STATE:
                    return "State";

                case FieldExpression.NAME:
                    return "WorkflowName";

                case FieldExpression.DUE_DATE:
                    return "DueDate";

                default:
                    return "1";
            }
        }

        private Type GetQueryClass(Query.Expression expr, IList classesCache)
        {
            if (classesCache == null)
            {
                classesCache = new ArrayList();
            }

            if (expr is FieldExpression)
            {
                FieldExpression fieldExpression = (FieldExpression)expr;

                switch (fieldExpression.Context)
                {
                    case FieldExpression.CURRENT_STEPS:
                        classesCache.Add(typeof(HibernateCurrentStep));

                        break;

                    case FieldExpression.HISTORY_STEPS:
                        classesCache.Add(typeof(HibernateHistoryStep));

                        break;

                    case FieldExpression.ENTRY:
                        classesCache.Add(typeof(HibernateWorkflowEntry));

                        break;

                    default:
                        throw new QueryNotSupportedException("Query for unsupported context " + fieldExpression.Context);
                }
            }
            else
            {
                NestedExpression nestedExpression = (NestedExpression)expr;

                for (int i = 0; i < nestedExpression.ExpressionCount; i++)
                {
                    CchenSoft.Workflow.Query.Expression expression = nestedExpression.GetExpression(i);

                    if (expression.IsNested())
                    {
                        classesCache.Add(GetQueryClass(nestedExpression.GetExpression(i), classesCache));
                    }
                    else
                    {
                        classesCache.Add(GetQueryClass(expression, classesCache));
                    }
                }
            }

            if (classesCache.Count > 1)
            {
                throw new QueryNotSupportedException("Store does not support nested queries of different types (types found:" + classesCache + ")");
            }

            return (Type)classesCache[0];
        }

        /**
         *  Recursive method for building Expressions using Query objects.
         */
        private ICriterion BuildExpression(WorkflowQuery query)
        {
            if (query.Left == null)
            {
                if (query.Right == null)
                {
                    return GetExpression(query); //leaf node
                }
                else
                {
                    throw new StoreException("Invalid WorkflowQuery object.  QueryLeft is null but QueryRight is not.");
                }
            }
            else
            {
                if (query.Right == null)
                {
                    throw new StoreException("Invalid WorkflowQuery object.  QueryLeft is not null but QueryRight is.");
                }

                switch (query.Operator)
                {
                    case WorkflowQuery.AND:
                        return NHibernate.Criterion.Expression.And(BuildExpression(query.Left), BuildExpression(query.Right));

                    case WorkflowQuery.OR:
                        return NHibernate.Criterion.Expression.Or(BuildExpression(query.Left), BuildExpression(query.Right));

                    case WorkflowQuery.XOR:
                        throw new StoreException("XOR Operator in Queries not supported by " + GetType().Name);

                    default:
                        throw new StoreException("Operator '" + query.Operator + "' is not supported by " + GetType().Name);
                }
            }
        }

        private ICriterion BuildNested(NestedExpression nestedExpression)
        {
            ICriterion full = null;

            for (int i = 0; i < nestedExpression.ExpressionCount; i++)
            {
                ICriterion expr;
                CchenSoft.Workflow.Query.Expression expression = nestedExpression.GetExpression(i);

                if (expression.IsNested())
                {
                    expr = BuildNested((NestedExpression)nestedExpression.GetExpression(i));
                }
                else
                {
                    FieldExpression sub = (FieldExpression)nestedExpression.GetExpression(i);
                    expr = QueryComparison(sub);

                    if (sub.Negate)
                    {
                        expr = NHibernate.Criterion.Expression.Not(expr);
                    }
                }

                if (full == null)
                {
                    full = expr;
                }
                else
                {
                    switch (nestedExpression.ExpressionOperator)
                    {
                        case NestedExpression.AND:
                            full = NHibernate.Criterion.Expression.And(full, expr);
                            break;

                        case NestedExpression.OR:
                            full = NHibernate.Criterion.Expression.Or(full, expr);
                            break;
                    }
                }
            }

            return full;
        }

        private ICriterion QueryComparison(FieldExpression expression)
        {
            switch (expression.Operator)
            {
                case FieldExpression.EQUALS:
                    return NHibernate.Criterion.Expression.Eq(GetFieldName(expression.Field), expression.Value);

                case FieldExpression.NOT_EQUALS:
                    return NHibernate.Criterion.Expression.Not(NHibernate.Criterion.Expression.Like(GetFieldName(expression.Field), expression.Value));

                case FieldExpression.GT:
                    return NHibernate.Criterion.Expression.Gt(GetFieldName(expression.Field), expression.Value);

                case FieldExpression.LT:
                    return NHibernate.Criterion.Expression.Lt(GetFieldName(expression.Field), expression.Value);

                default:
                    return NHibernate.Criterion.Expression.Eq(GetFieldName(expression.Field), expression.Value);
            }
        }
    }
}
